/**
 *  Copyright 2011 Alexandru Craciun, Eyal Kaspi
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.stjs.generator;

import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;

import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;

import org.stjs.generator.GenerationContext.AnnotationCacheKey;
import org.stjs.generator.javac.CustomClassloaderJavaFileManager;
import org.stjs.generator.javascript.JavaScriptBuilder;
import org.stjs.generator.javascript.rhino.RhinoJavaScriptBuilder;
import org.stjs.generator.name.DefaultJavaScriptNameProvider;
import org.stjs.generator.name.JavaScriptNameProvider;
import org.stjs.generator.plugin.GenerationPlugins;
import org.stjs.generator.utils.ClassUtils;
import org.stjs.generator.utils.Timers;

import com.google.common.collect.Maps;
import com.google.common.io.Closeables;
import com.google.common.io.Files;
import com.google.common.io.InputSupplier;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.Trees;
import com.sun.tools.javac.api.JavacTool;

/**
 * This class parses a Java source file, launches several visitors and finally
 * generate the corresponding Javascript.
 * 
 * @author acraciun
 */
public class Generator {
	private static final int EXECUTOR_TERMINAL_TIMEOUT = 10;
	private static final String STJS_FILE = "stjs.js";
	private final GenerationPlugins<Object> plugins;
	private StandardJavaFileManager fileManager;
	private JavaFileManager classLoaderFileManager;
	private final Map<AnnotationCacheKey, Object> cacheAnnotations = Maps
			.newHashMap();
	private Executor taskExecutor;
	private String sourceEncoding;

	public Generator() {
		plugins = new GenerationPlugins<Object>();
	}

	@SuppressWarnings("PMD.DoNotUseThreads")
	public void init(ClassLoader builtProjectClassLoader, String sourceEncoding) {
		// nothing to do
		cacheAnnotations.clear();
		// taskExecutor = Executors.newFixedThreadPool(4);
		taskExecutor = new Executor() {
			@Override
			public void execute(Runnable command) {
				command.run();
			}
		};
		if (sourceEncoding == null) {
			this.sourceEncoding = Charset.defaultCharset().name();
		} else {
			this.sourceEncoding = sourceEncoding;
		}
	}

	public void close() {
		Closeables.closeQuietly(fileManager);
		if (taskExecutor instanceof ExecutorService) {
			ExecutorService es = (ExecutorService) taskExecutor;
			es.shutdown();
			try {
				es.awaitTermination(EXECUTOR_TERMINAL_TIMEOUT, TimeUnit.SECONDS);
			} catch (InterruptedException e) {
				return;
			}
		}
	}

	public File getOutputFile(File generationFolder, String className) {
		return getOutputFile(generationFolder, className, true);
	}

	public File getSourceMapFile(File generationFolder, String className) {
		return new File(generationFolder, className.replace('.',
				File.separatorChar) + ".map");
	}

	public File getOutputFile(File generationFolder, String className,
			boolean generateDirectory) {
		File output = new File(generationFolder, className.replace('.',
				File.separatorChar) + ".js");
		if (generateDirectory && !output.getParentFile().exists()
				&& !output.getParentFile().mkdirs()) {
			throw new STJSRuntimeException(
					"Unable to create parent folder for the output file:"
							+ output);
		}
		return output;
	}

	public File getInputFile(File sourceFolder, String className) {
		return new File(sourceFolder,
				className.replace('.', File.separatorChar) + ".java");
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	private JavaScriptBuilder<Object> getJavaScriptBuilder() {
		// TODO: here we my return SourceJavaScript builder as well.
		return (JavaScriptBuilder) new RhinoJavaScriptBuilder();
	}

	/**
	 * @param builtProjectClassLoader
	 * @param inputFile
	 * @param outputFile
	 * @param configuration
	 * @return the list of imports needed by the generated class
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public ClassWithJavascript generateJavascript(
			ClassLoader builtProjectClassLoader, String className,
			File sourceFolder, GenerationDirectory generationFolder,
			File targetFolder, GeneratorConfiguration configuration)
			throws JavascriptFileGenerationException {

		DependencyResolver dependencyResolver = new GeneratorDependencyResolver(
				builtProjectClassLoader, sourceFolder, generationFolder,
				targetFolder, configuration);

		Class<?> clazz = ClassUtils
				.getClazz(builtProjectClassLoader, className);
		if (ClassUtils.isBridge(builtProjectClassLoader, clazz)) {
			return new BridgeClass(dependencyResolver, clazz);
		}

		File inputFile = getInputFile(sourceFolder, className);
		File outputFile = getOutputFile(generationFolder.getAbsolutePath(),
				className);
		JavaScriptNameProvider names = new DefaultJavaScriptNameProvider();
		GenerationPlugins<Object> currentClassPlugins = plugins.forClass(clazz);

		GenerationContext<Object> context = new GenerationContext<Object>(
				inputFile, configuration, names, null, builtProjectClassLoader,
				cacheAnnotations, getJavaScriptBuilder());

		CompilationUnitTree cu = parseAndResolve(inputFile, context,
				builtProjectClassLoader, configuration.getSourceEncoding());

		// check the code
		Timers.start("check-java");
		currentClassPlugins.getCheckVisitor().scan(cu,
				(GenerationContext) context);
		context.getChecks().check();
		Timers.end("check-java");

		// generate the javascript code
		Timers.start("write-js-ast");
		Object javascriptRoot = currentClassPlugins.getWriterVisitor().scan(cu,
				context);
		// check for any error arriving during writing
		context.getChecks().check();
		Timers.end("write-js-ast");

		STJSClass stjsClass = new STJSClass(dependencyResolver, targetFolder,
				className);
		Set<String> resolvedClasses = new LinkedHashSet<String>(
				names.getResolvedTypes());
		resolvedClasses.remove(className);
		stjsClass.setDependencies(resolvedClasses);
		stjsClass.setGeneratedJavascriptFile(relative(generationFolder,
				className));

		// dump the ast to a file
		taskExecutor.execute(new DumpFilesTask<Object>(outputFile, context,
				javascriptRoot, stjsClass, generationFolder, configuration
						.isGenerateSourceMap()));

		return stjsClass;
	}

	private URI relative(GenerationDirectory generationFolder, String className) {
		// FIXME temporary have to remove the full path from the file name.
		// it should be different depending on whether the final artifact is a
		// war or a jar.
		// String path = generationFolder.getPath();
		// int pos = path.lastIndexOf("target");
		try {
			File file = getOutputFile(
					generationFolder.getRelativeToClasspath(), className, false);
			String path = file.getPath().replace('\\', '/');
			// make it "absolute"
			if (!path.startsWith("/")) {
				path = "/" + path;
			}
			return new URI(path);

			// if (pos >= 0) {
			// File file = getOutputFile(new File(path.substring(pos)),
			// className);
			// return new URI(file.getPath().replace('\\', '/'));
			// }
			// return getOutputFile(generationFolder, className).toURI();
		} catch (URISyntaxException e) {
			throw new JavascriptClassGenerationException(className, e);
		}
	}

	private JavaCompiler getCompiler(ClassLoader builtProjectClassLoader,
			String sourceEncoding) {
		// create it directly to avoid ClassLoader problems
		JavaCompiler compiler = JavacTool.create();
		if (compiler == null) {
			throw new STJSRuntimeException(
					"A Java compiler is not available for this project. You may have configured your environment to run with JRE instead of a JDK");
		}
		if (fileManager == null) {
			fileManager = compiler.getStandardFileManager(null, null,
					Charset.forName(sourceEncoding));
			classLoaderFileManager = new CustomClassloaderJavaFileManager(
					builtProjectClassLoader, fileManager);
		}
		return compiler;
	}

	private <JS> CompilationUnitTree parseAndResolve(File inputFile,
			GenerationContext<JS> context, ClassLoader builtProjectClassLoader,
			String sourceEncoding) {
		JavaCompiler.CompilationTask task = null;
		JavacTask javacTask = null;
		try {
			JavaCompiler compiler = getCompiler(builtProjectClassLoader,
					sourceEncoding);
			Iterable<? extends JavaFileObject> fileObjects = fileManager
					.getJavaFileObjectsFromFiles(Collections
							.singleton(inputFile));
			List<String> options = Arrays.asList("-proc:none");
			task = compiler.getTask(null, classLoaderFileManager, null,
					options, null, fileObjects);
			javacTask = (JavacTask) task;

			context.setTrees(Trees.instance(javacTask));
			context.setElements(javacTask.getElements());
			context.setTypes(javacTask.getTypes());

			Timers.start("parse-java");
			CompilationUnitTree cu = javacTask.parse().iterator().next();
			Timers.end("parse-java");

			Timers.start("analyze-java");
			javacTask.analyze();
			Timers.end("analyze-java");

			context.setCompilationUnit(cu);

			return cu;
		} catch (IOException e) {
			throw new JavascriptFileGenerationException(new SourcePosition(
					context.getInputFile(), 0, 0),
					"Cannot parse the Java file:" + e);
		}

	}

	/**
	 * This method copies the Javascript support file (stjs.js currently) to the
	 * desired folder. This method should be called after the processing of all
	 * the files.
	 * 
	 * @param folder
	 */
	public void copyJavascriptSupport(File folder) {
		final InputStream stjs = Thread.currentThread().getContextClassLoader()
				.getResourceAsStream(STJS_FILE);
		if (stjs == null) {
			throw new STJSRuntimeException(STJS_FILE
					+ " is missing from the Generator's classpath");
		}
		File outputFile = new File(folder, STJS_FILE);
		try {
			Files.copy(new InputStreamSupplier(stjs), outputFile);
		} catch (IOException e) {
			throw new STJSRuntimeException("Could not copy the " + STJS_FILE
					+ " file to the folder " + folder + ":" + e.getMessage(), e);
		} finally {
			Closeables.closeQuietly(stjs);
		}
	}

	private static final class InputStreamSupplier implements
			InputSupplier<InputStream> {
		private final InputStream input;

		public InputStreamSupplier(InputStream input) {
			this.input = input;
		}

		@Override
		public InputStream getInput() throws IOException {
			return input;
		}

	}

	/**
	 * this class lazily generates the dependencies
	 */
	private class GeneratorDependencyResolver implements DependencyResolver {
		private final ClassLoader builtProjectClassLoader;
		private final File sourceFolder;
		private final GenerationDirectory generationFolder;
		private final File targetFolder;
		private final GeneratorConfiguration configuration;

		public GeneratorDependencyResolver(ClassLoader builtProjectClassLoader,
				File sourceFolder, GenerationDirectory generationFolder,
				File targetFolder, GeneratorConfiguration configuration) {
			this.builtProjectClassLoader = builtProjectClassLoader;
			this.sourceFolder = sourceFolder;
			this.targetFolder = targetFolder;
			this.generationFolder = generationFolder;
			this.configuration = configuration;
		}

		private void checkFolders(String parentClassName) {
			if (generationFolder == null || sourceFolder == null
					|| targetFolder == null) {
				throw new IllegalStateException(
						"This resolver assumed that the javascript for the class ["
								+ parentClassName + "] was already generated");
			}
		}

		@Override
		public ClassWithJavascript resolve(String className) {
			String parentClassName = className;
			int pos = parentClassName.indexOf('$');
			if (pos > 0) {
				parentClassName = parentClassName.substring(0, pos);
			}
			// try first if to see if it's a bridge class
			Class<?> clazz;
			try {
				clazz = builtProjectClassLoader.loadClass(parentClassName);
			} catch (ClassNotFoundException e) {
				throw new STJSRuntimeException(e);
			}
			if (ClassUtils.isBridge(builtProjectClassLoader, clazz)) {
				return new BridgeClass(this, clazz);
			}

			// check if it has already generated
			STJSClass stjsClass = new STJSClass(this, builtProjectClassLoader,
					parentClassName);
			if (stjsClass.getJavascriptFiles().isEmpty()) {
				checkFolders(parentClassName);
				stjsClass = (STJSClass) generateJavascript(
						builtProjectClassLoader, parentClassName, sourceFolder,
						generationFolder, targetFolder, configuration);
			}
			return stjsClass;
		}

	}

	/**
	 * This method assumes the javascript code for the given class was already
	 * generated
	 * 
	 * @param testClass
	 */
	public ClassWithJavascript getExistingStjsClass(ClassLoader classLoader,
			Class<?> testClass) {
		return new GeneratorDependencyResolver(classLoader, null, null, null,
				null).resolve(testClass.getName());
	}

	@SuppressWarnings("PMD.DoNotUseThreads")
	private class DumpFilesTask<JS> implements Runnable {
		private final File outputFile;
		private final GenerationContext<JS> context;
		private final JS javascriptRoot;
		private final STJSClass stjsClass;
		private final GenerationDirectory generationFolder;
		private final boolean generateSourceMap;

		public DumpFilesTask(File outputFile, GenerationContext<JS> context,
				JS javascriptRoot, STJSClass stjsClass,
				GenerationDirectory generationFolder, boolean generateSourceMap) {
			this.outputFile = outputFile;
			this.context = context;
			this.javascriptRoot = javascriptRoot;
			this.stjsClass = stjsClass;
			this.generationFolder = generationFolder;
			this.generateSourceMap = generateSourceMap;
		}

		@Override
		public void run() {
			writeJavaScript();
			writePropertiesFile();
			writeSourceMap();
		}

		private void writeJavaScript() {
			BufferedWriter writer = null;
			try {
				Timers.start("dump-js");
				writer = Files.newWriter(outputFile,
						Charset.forName(sourceEncoding));
				context.writeJavaScript(javascriptRoot, writer);
				writer.flush();
				Timers.end("dump-js");
			} catch (IOException e) {
				throw new STJSRuntimeException("Could not open output file "
						+ outputFile + ":" + e, e);
			} finally {
				Closeables.closeQuietly(writer);
			}
		}

		// write properties

		private void writePropertiesFile() {
			Timers.start("write-props");
			stjsClass.store();
			Timers.end("write-props");
		}

		private void writeSourceMap() {
			if (generateSourceMap) {
				BufferedWriter sourceMapWriter = null;

				try {
					// write the source map
					sourceMapWriter = Files.newWriter(
							getSourceMapFile(
									generationFolder.getAbsolutePath(),
									stjsClass.getClassName()), Charset
									.forName(sourceEncoding));
					context.writeSourceMap(sourceMapWriter);
					sourceMapWriter.flush();

					// copy the source aside the generated js to be able to have
					// it delivered to the browser for
					// debugging
					Files.copy(context.getInputFile(),
							new File(outputFile.getParentFile(), context
									.getInputFile().getName()));
					// copy the STJS properties file in the same folder as the
					// Javascript file (if this folder is
					// different
					// to be
					// able to do backward analysis: i.e fine the class name
					// corresponding to a JS)
					File stjsPropFile = stjsClass.getStjsPropertiesFile();
					File copyStjsPropFile = new File(
							generationFolder.getAbsolutePath(),
							ClassUtils.getPropertiesFileName(stjsClass
									.getClassName()));
					if (!stjsPropFile.equals(copyStjsPropFile)) {
						Files.copy(stjsPropFile, copyStjsPropFile);
					}
				} catch (IOException e) {
					throw new STJSRuntimeException("Could generate source map:"
							+ e, e);
				} finally {
					if (sourceMapWriter != null) {
						Closeables.closeQuietly(sourceMapWriter);
					}
				}
			}
		}
	}

}
